﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Net;
using System.IO;
using System.Collections.ObjectModel;
using System.Diagnostics;

namespace OAuthAccess
{
    /// <summary>
    /// If Access Token is available, you can access Protected Resources by using this class.<br/>
    /// If Access Token is not available, you need get Access Token by using OAuthAuthorizationService class.
    /// </summary>
    public class OAuthRequest : INotReadyOAuthRequest,IReadyOAuthRequest
    {
        private List<Parameter> parameterList = new List<Parameter>();

        private OAuthRequest(Consumer consumer, AccessToken accessToken)
        {
            Parameters = new ReadOnlyCollection<Parameter>(parameterList);
            Consumer = consumer;
            AccessToken = accessToken;
        }

        /// <summary>
        /// Begins construction of OAuthRequest with the Consumer information and AccessToken.  
        /// </summary>
        /// <param name="consumer">The instance of Consumer Class.</param>
        /// <param name="accessToken">The instance of AccessToken Class.</param>
        /// <returns>INotReadyOAuthRequest Instance.</returns>
        public static INotReadyOAuthRequest Create(Consumer consumer, AccessToken accessToken)
        {
            if (consumer == null || accessToken == null)
            {
                throw new ArgumentException("Can not Null Consumer and AccessToken");
            }

            return new OAuthRequest(consumer, accessToken);
        }

        /// <summary>
        /// Gets Consumer Information.
        /// </summary>
        public Consumer Consumer
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets Access Token.
        /// </summary>
        public AccessToken AccessToken
        {
            get;
            private set;
        }

        /// <summary>
        /// Gets Request Url.
        /// </summary>
        public string RequestUrl
        {
            get;
            internal set;
        }

        /// <summary>
        /// Gets Realm.
        /// </summary>
        public string Realm
        {
            get;
            internal set;
        }

        /// <summary>
        /// Gets Http Method Type of your request.
        /// </summary>
        public HttpMethodType MethodType
        {
            get;
            internal set;
        }

        /// <summary>
        /// Gets Parameter read only lists.
        /// </summary>
        public ReadOnlyCollection<Parameter> Parameters
        {
            get;
            private set;
        }

        internal List<Parameter> ParameterList
        {
            get
            {
                return parameterList;
            }
            set
            {
                parameterList = value;
            }
        }


        /// <summary>
        /// Gets retry count of your request.
        /// While retrying, if exeception is thrown, break retrying.
        /// if you don't specify IgnoreStatusCodeCondition, this value will be Ignored.
        /// </summary>
        public int RetryCount
        {
            get;
            internal set;
        }

        /// <summary>
        /// The Predicate{int} delegate that defines 
        /// the conditions of that you want to ignore network error that have specific HTTP StatusCode.<br/>
        /// If network error happend that have specific Http Status Code, 
        /// it will return OAuthResponse that IsSuccess property is set false and it won't throw Exception.<br/>
        /// When you don't specified it,thrown exception will be intact.
        /// </summary>
        public Predicate<int> IgnoreStatusCodeCondition
        {
            get;
            internal set;
        }

        /// <summary>
        /// Gets OAuth Authorization Header as String.
        /// </summary>
        public string AuthorizationHeader
        {
            get;
            internal set;
        }

        /// <summary>
        /// Gets Proxy.
        /// </summary>
        public WebProxy Proxy
        {
            get;
            internal set;
        }


        internal OAuthRequest DeepCopy()
        {
            OAuthRequest newReq = new OAuthRequest(Consumer,AccessToken);
            newReq.RequestUrl = RequestUrl;
            newReq.MethodType = MethodType;
            newReq.Realm = Realm;

            foreach (Parameter p in Parameters)
            {
                newReq.ParameterList.Add(p);
            }

            newReq.RetryCount = RetryCount;
            newReq.IgnoreStatusCodeCondition = IgnoreStatusCodeCondition;
            newReq.Proxy = Proxy;
            newReq.AuthorizationHeader = AuthorizationHeader;
            return newReq;
        }

    }

    /// <summary>
    /// Represents request of building on the way.
    /// It is not commited until you call CommitRequest Method. 
    /// </summary>
    public interface INotReadyOAuthRequest { }

    /// <summary>
    /// Represents implements of INotReadyOAuthRequest interface.
    /// </summary>
    public static class INotReadyOAuthRequestExtension
    {
        /// <summary>
        /// Sets Realm.it can be overwritten by recall this method.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest.</param>
        /// <param name="realm">The String of Realm.You can also specify Null.</param>
        /// <returns>The new instance of INotReadyOAuthRequest that deep copied and set.</returns>
        public static INotReadyOAuthRequest SetRealm(this INotReadyOAuthRequest req, string realm)
        {
            OAuthRequest r = req as OAuthRequest;
            OAuthRequest newReq = r.DeepCopy();
            newReq.Realm = realm;
            return newReq;
        }

        /// <summary>
        /// Sets Request Url that not contain Parameter.it can be overwritten by recall this method.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest.</param>
        /// <param name="url">The String of Request Url.</param>
        /// <returns>The new instance of INotReadyOAuthRequest that deep copied and set.</returns>
        public static INotReadyOAuthRequest SetUrl(this INotReadyOAuthRequest req, string url)
        {
            OAuthRequest r = req as OAuthRequest;
            OAuthRequest newReq = r.DeepCopy();
            newReq.RequestUrl = url;
            return newReq;
        }

        /// <summary>
        /// Sets HTTP Method Type of your request(GET or POST). it can be overwritten by recall this method.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest</param>
        /// <param name="type">HttpMethodType. you can't set UnKnown</param>
        /// <returns>The new instance of INotReadyOAuthRequest that deep copied and set.</returns>
        public static INotReadyOAuthRequest SetHttpMethod(this INotReadyOAuthRequest req, HttpMethodType type)
        {
            if (type == HttpMethodType.UnKnown)
            {
                throw new ArgumentException("You can not set UnKnown.");
            }

            OAuthRequest r = req as OAuthRequest;
            OAuthRequest newReq = r.DeepCopy();
            newReq.MethodType = type;
            return newReq;
        }

        /// <summary>
        /// Sets Proxy.it can be overwritten by recall this method.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest</param>
        /// <param name="proxy">Proxy.You can also specify Null.</param>
        /// <returns>The new instance of INotReadyOAuthRequest that deep copied and set.</returns>
        public static INotReadyOAuthRequest SetProxy(this INotReadyOAuthRequest req, WebProxy proxy)
        {
            OAuthRequest r = req as OAuthRequest;
            OAuthRequest newReq = r.DeepCopy();
            newReq.Proxy = proxy;
            return newReq;
        }

        /// <summary>
        /// Adds the Parameter to your request.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest</param>
        /// <param name="args">The array of Parameter instance.</param>
        /// <returns>The new instance of INotReadyOAuthRequest that deep copied and set.</returns>
        public static INotReadyOAuthRequest AddParameter(this INotReadyOAuthRequest req, params Parameter[] args)
        {
            OAuthRequest r = req as OAuthRequest;

            OAuthRequest newReq = r.DeepCopy();

            foreach (Parameter p in args)
            {
                newReq.ParameterList.Add(p);
            }

            return newReq;
        }

        /// <summary>
        /// Clears all Parameter of your request.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest</param>
        /// <returns>The new instance of INotReadyOAuthRequest that deep copied and set.</returns>
        public static INotReadyOAuthRequest ClearParameter(this INotReadyOAuthRequest req)
        {
            OAuthRequest r = req as OAuthRequest;
            OAuthRequest newReq = r.DeepCopy();
            newReq.ParameterList.Clear();
            return newReq;
        }

        /// <summary>
        /// Removes all Parameter that have specified Key.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest</param>
        /// <param name="parameterKey">The string of Parameter Key</param>
        /// <returns>The new instance of INotReadyOAuthRequest that deep copied and set.</returns>
        public static INotReadyOAuthRequest RemoveParameter(this INotReadyOAuthRequest req, string parameterKey)
        {
            OAuthRequest r = req as OAuthRequest;
            OAuthRequest newReq = r.DeepCopy();
            var targets = newReq.ParameterList.FindAll((p) =>
            {
                return parameterKey == p.Key;
            });

            foreach (Parameter p in targets)
            {
                newReq.ParameterList.Remove(p);
            }
            return newReq;
        }

        /// <summary>
        /// Removes the Parameter at the specified index.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest</param>
        /// <param name="index">The zero-based index of the Parameter to remove. </param>
        /// <returns>The new instance of INotReadyOAuthRequest that deep copied and set.</returns>
        public static INotReadyOAuthRequest RemoveAtParameter(this INotReadyOAuthRequest req, int index)
        {
            OAuthRequest r = req as OAuthRequest;
            OAuthRequest newReq = r.DeepCopy();
            newReq.ParameterList.RemoveAt(index);
            return newReq;
        }

        /// <summary>
        /// Sets max Retry Count.<br/>
        /// While retrying, if exeception is thrown, break retrying.
        /// if you don't specify IgnoreStatusCodeCondition, this value will be Ignored.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest</param>
        /// <param name="count">The max retry count. You can also specify 0.</param>
        /// <returns>The new instance of INotReadyOAuthRequest that deep copied and set.</returns>
        public static INotReadyOAuthRequest SetRetryCount(this INotReadyOAuthRequest req,int count)
        {
            OAuthRequest newReq = (req as OAuthRequest).DeepCopy();
            newReq.RetryCount = count;
            return newReq;
        }

        /// <summary>
        /// The Predicate{int} delegate that defines 
        /// the conditions of that you want to ignore network error that have specific HTTP StatusCode.<br/>
        /// If network error happend that have specific HTTP Status Code, 
        /// it will return OAuthResponse that IsSuccess property is set false and it won't throw Exception.<br/>
        /// When you don't specified it,thrown exception will be intact.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest</param>
        /// <param name="condition">The Predicate{int} delegate. You can also specify Null.</param>
        /// <returns>The new instance of INotReadyOAuthRequest that deep copied and set.</returns>
        public static INotReadyOAuthRequest SetIgnoreStatusCodeCondition(this INotReadyOAuthRequest req, Predicate<int> condition)
        {
            OAuthRequest newReq = (req as OAuthRequest).DeepCopy();
            newReq.IgnoreStatusCodeCondition = condition;
            return newReq;
        }

        /// <summary>
        /// Commits Request.<br/>
        /// Before you call this method. you must at least call SetUrl and SetHttpMethod. 
        /// When this method call,Authorization Header made.<br/>
        /// After this method called,you can call GetResponse method.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest</param>
        /// <returns>The new instance of IReadyOAuthRequest.</returns>
        public static IReadyOAuthRequest CommitRequest(this INotReadyOAuthRequest req)
        {
            OAuthRequest r = req as OAuthRequest;

            if (r.MethodType == HttpMethodType.UnKnown || string.IsNullOrEmpty(r.RequestUrl))
            {
                throw new ArgumentException("Must Call SetUrl and SetHttpMethod!");
            }

            OAuthRequest newReq = r.DeepCopy();

            newReq.AuthorizationHeader = OAuthUtility.GenerateAuthorizationHeader(
                r.Realm,
                r.RequestUrl,
                r.MethodType,
                r.Consumer,
                r.AccessToken,
                null,
                r.Parameters);

            return newReq;
        }
    }

    /// <summary>
    /// Represents request was commited.
    /// </summary>
    public interface IReadyOAuthRequest
    {
        /// <summary>
        /// Represents string of AuthorizationHeader.
        /// </summary>
        string AuthorizationHeader
        {
            get;
        }
    }

    /// <summary>
    /// Represents implements of IReadyOAuthRequest interface.
    /// </summary>
    public static class IReadyOAuthRequestExtension
    {
        /// <summary>
        /// Gets response.
        /// </summary>
        /// <param name="req">The instance of INotReadyOAuthRequest.</param>
        /// <returns>The Instance og OAuthResponse.</returns>
        public static OAuthResponse GetResponse(this IReadyOAuthRequest req)
        {
            OAuthRequest newReq;
            HttpWebRequest httpReq;
            ReadyResponse(req, out newReq, out httpReq);

            int count = 0;

            return RetryAccess(newReq, httpReq, ref count);
        }

        public static Stream GetReponseStream(this IReadyOAuthRequest req)
        {
            OAuthRequest newReq;
            HttpWebRequest httpReq;
            ReadyResponse(req, out newReq, out httpReq);

            return httpReq.GetResponse().GetResponseStream();
        }

        private static void ReadyResponse(IReadyOAuthRequest req, out OAuthRequest newReq, out HttpWebRequest httpReq)
        {
            OAuthRequest r = req as OAuthRequest;

            newReq = r.DeepCopy();

            if (newReq.MethodType == HttpMethodType.Get)
            {
                string newUrl = newReq.RequestUrl;
                if (newReq.Parameters.Count != 0)
                {
                    newUrl += "?";
                }
                newUrl = newUrl + string.Join("&", newReq.Parameters.Select((p) => { return p.Key + "=" + p.Value.UrlEncode(); }).ToArray());
                httpReq = WebRequest.Create(newUrl) as HttpWebRequest;
                httpReq.Method = "GET";
            }
            else
            {
                httpReq = WebRequest.Create(newReq.RequestUrl) as HttpWebRequest;
                httpReq.Method = "POST";
                httpReq.ContentType = "application/x-www-form-urlencoded";
            }

            if (newReq.Proxy != null)
            {
                httpReq.Proxy = newReq.Proxy;
            }

            httpReq.Headers["Authorization"] = newReq.AuthorizationHeader;

            if (httpReq.Method == "POST")
            {
                string para = string.Join("&", newReq.Parameters.Select((p) => { return p.Key + "=" + p.Value.UrlEncode(); }).ToArray());
                byte[] paraBytes = Encoding.ASCII.GetBytes(para);

                httpReq.ContentLength = paraBytes.Length;

                using (Stream s = httpReq.GetRequestStream())
                {
                    s.Write(paraBytes, 0, paraBytes.Length);
                    s.Flush();
                }
            }
        }


        private static OAuthResponse RetryAccess(this IReadyOAuthRequest oAuthRequest, HttpWebRequest req, ref int retryCount)
        {
            try
            {
                return new OAuthResponse(req.GetResponse() as HttpWebResponse,retryCount);
            }
            catch (WebException we)
            {
                if (we.Status != WebExceptionStatus.ProtocolError)
                {
                    throw new OAuthAccessException(we.Message,we);
                }

                HttpWebResponse res = we.Response as HttpWebResponse;

                if ((oAuthRequest as OAuthRequest).IgnoreStatusCodeCondition == null)
                {
                    throw new OAuthAccessException(we.Message, we);
                }
                else if ((oAuthRequest as OAuthRequest).IgnoreStatusCodeCondition(((int)res.StatusCode)))
                {
                    retryCount++;

                    if (retryCount > (oAuthRequest as OAuthRequest).RetryCount)
                    {
                        return new OAuthResponse(res, false,retryCount - 1);
                    }

                    Debug.WriteLine("Retried " + (oAuthRequest as OAuthRequest).MethodType.ToString() + 
                        " URL :" + (oAuthRequest as OAuthRequest).RequestUrl+
                        " RetryCount :" +  retryCount.ToString());

                    return oAuthRequest.RetryAccess(req, ref retryCount);
                }
                else
                {
                    throw new OAuthAccessException(we.Message, we);
                }
            }
        }
    }

    public class MyPolicy : ICertificatePolicy
    {
        public bool CheckValidationResult(
          ServicePoint srvPoint
         , System.Security.Cryptography.X509Certificates.X509Certificate certificate
         , WebRequest request
         , int certificateProblem)
        {

            Debug.WriteLine("certificateProblem = " + certificateProblem.ToString());

            // 信用したことにする
            return true;
        }
    }

}
